﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Gunningine
{
    /// <summary>
    /// Map tile.
    /// </summary>
    public class Tile : IQuadtreeItem
    {
        /// <summary>
        /// The type of tile.
        /// </summary>
        public int Type { get; private set; }

        public int X { get; private set; }

        public int Y { get; private set; }

        /// <summary>
        /// bounding area.
        /// </summary>
        private Vector3 boundsMin, boundsMax;

        /// <summary>
        /// Constructor.
        /// </summary>
        public Tile(int type, int x, int y, Vector3 boundsMin, Vector3 boundsMax)
        {
            this.Type = type;
            this.X = x;
            this.Y = y;
            this.boundsMin = boundsMin;
            this.boundsMax = boundsMax;
        }

        #region IQuadtreeItem implementation

        public Vector3 BoundsMin
        {
            get { return this.boundsMin; }
        }
            
        public Vector3 BoundsMax
        {
            get { return this.boundsMax; }
        }

        #endregion
    }

    public class Map : IEntityContainer, IAlignmentTester
    {
        private Tile[] tiles = null;

        private Quadtree tree = null;

        private HashSet<Entity> entities = new HashSet<Entity>();

        private HashSet<Entity> playerControlledEntities = new HashSet<Entity>();

        private Dictionary<int, HashSet<Entity>> alignedEntities = new Dictionary<int, HashSet<Entity>>();

        private ScriptManager scriptMgr = null;

        private ResourceManager resourceMgr = null;

        private WorldState worldState;

        private Battery battery;

        public int TilesX { get; private set; }

        public int TilesY { get; private set; }

        public float Scale { get; private set; }

        public float SizeX
        {
            get { return this.TilesX * this.Scale; }
        }

        public float SizeY
        {
            get { return this.TilesY * this.Scale; }
        }

        internal HashSet<Entity> Entities
        {
            get { return this.entities; }
        }

        /// <summary>
        /// Acceleration grid for entities.
        /// </summary>
        internal EntityGrid Grid { get; private set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="tilesX">X dimension, in tiles.</param>
        /// <param name="tilesY">Y dimension, in tiles.</param>
        /// <param name="scale">size of a tile in world units.</param>
        public Map(int tilesX, int tilesY, float scale, ScriptManager scriptMgr, ResourceManager resourceMgr, WorldState worldState, Battery battery)
        {
            this.TilesX = tilesX;
            this.TilesY = tilesY;
            this.Scale = scale;
            this.scriptMgr = scriptMgr;
            this.resourceMgr = resourceMgr;
            this.worldState = worldState;
            this.battery = battery;

            // Create tiles and acceleration grid.
            this.tiles = new Tile[this.TilesX * this.TilesY];
            this.Grid = new EntityGrid(0, 0, this.TilesX * this.Scale, this.TilesY * this.Scale, 64, 64);

            // Default tiles to 1.
            int index = 0;
            for (int y = 0; y < tilesY; ++y)
            {
                for (int x = 0; x < tilesX; ++x)
                {
                    Vector3 boundsMin = new Vector3(x * this.Scale, y * this.Scale, 0);
                    Vector3 boundsMax = new Vector3((x + 1) * this.Scale, (y + 1) * this.Scale, 512);

                    this.tiles[index] = new Tile(1, x, y, boundsMin, boundsMax);
                    index++;
                }
            }

            // Build quad tree
            this.tree = new Quadtree(this.tiles, 9, 8);
        }

        /// <summary>
        /// Find a random location in the map which is not in geometry.
        /// </summary>
        /// <param name="rng">random number generator.</param>
        /// <returns>location.</returns>
        public Vector3 GetRandomSpawnLocation(Random rng)
        {
            float x = rng.Next(this.TilesX) * this.Scale;
            float y = rng.Next(this.TilesY) * this.Scale;

            return new Vector3(x, y, 0.0f);
        }

        /// <summary>
        /// Create a new entity.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="position"></param>
        /// <param name="direction"></param>
        /// <param name="model"></param>
        /// <param name="flags"></param>
        public Entity CreateEntity(string type, Vector3 position, Vector3 direction, int alignment)
        {
            Entity ent = this.scriptMgr.CreateType<Entity>(type, position, direction, this.worldState, alignment);
            ent.SetCore(this.resourceMgr);
            ent.Activate(this.Grid, this.battery, this, this);
            ent.SetState(ent.InitialState);
                
            return ent;
        }

        /// <summary>
        /// Sets an entity as player controlled, and will not be updated
        /// by the core.
        /// </summary>
        /// <param name="entity"></param>
        public void SetEntityPlayerControlled(Entity entity, bool set)
        {
            if (set)
                this.playerControlledEntities.Add(entity);
            else
                this.playerControlledEntities.Remove(entity);
        }

        /// <summary>
        /// Update map.
        /// </summary>
        public void Update()
        {
            foreach (var entity in this.entities)
            {
                entity.UpdateLogic();
            }

            var entitiesToUpdate = this.entities.Except(this.playerControlledEntities);
            foreach (Entity entity in entitiesToUpdate)
            {
                entity.CheckEvents();
                entity.UpdateControl();
            }
        }

        /// <summary>
        /// Render map.
        /// </summary>
        /// <param name="renderSystem"></param>
        public int RenderWorld(RenderSystem renderSystem, Camera camera, Vector2 aimingTarget)
        {
            Texture groundTexture = renderSystem.GetTexture("Textures/e6tileceiling");
            groundTexture.Bind();

            Shader worldShader = renderSystem.GetShader("WorldShader");
            worldShader.Activate();

            worldShader.SetValueUniform("tex", 0);
            worldShader.SetValueUniform("targetPosition", aimingTarget);
            worldShader.SetValueUniform("targetRadius", 30.0f);

            renderSystem.SetColour(1.0f, 1.0f, 1.0f, 1.0f);

            Vector2 t0 = Vector2.Zero;
            Vector2 t1 = Vector2.UnitX;
            Vector2 t2 = Vector2.One;
            Vector2 t3 = Vector2.UnitY;

            Vector3 p0 = new Vector3(0, 0, 0);
            Vector3 p1 = new Vector3(0, 0, 0);
            Vector3 p2 = new Vector3(0, 0, 0);
            Vector3 p3 = new Vector3(0, 0, 0);

            // Get all tiles in view
            List<IQuadtreeItem> tilesInView = this.tree.GetItemsInFrustum(renderSystem, camera);

            foreach (var tile in tilesInView)
            {
                Tile t = (Tile)tile;

                p0.X = t.X * this.Scale;
                p0.Y = t.Y * this.Scale;
                p1.X = (t.X + 1) * this.Scale;
                p1.Y = t.Y * this.Scale;
                p2.X = (t.X + 1) * this.Scale;
                p2.Y = (t.Y + 1) * this.Scale;
                p3.X = t.X * this.Scale;
                p3.Y = (t.Y + 1) * this.Scale;
                renderSystem.RenderTexturedQuad3d(p0, t0, p1, t1, p2, t2, p3, t3);
            }

            groundTexture.Unbind();
            worldShader.Deactivate();

            return tilesInView.Count;
        }

        /// <summary>
        /// Render entities.
        /// </summary>
        /// <param name="renderSystem"></param>
        /// <param name="camera"></param>
        /// <param name="aimingTarget"></param>
        public int RenderEntities(RenderSystem renderSystem, Camera camera, Vector2 aimingTarget)
        {
            Shader entityShader = renderSystem.GetShader("EntityShader");
            entityShader.Activate();

            entityShader.SetValueUniform("tex", 0);
            entityShader.SetValueUniform("targetPosition", aimingTarget);
            entityShader.SetValueUniform("targetRadius", 30.0f);

            // Get all entities in view
            List<Entity> entitiesInView = this.Grid.GetEntitiesInFrustum(renderSystem, camera);
            foreach (Entity entity in entitiesInView)
            {
                entity.Render(renderSystem);
            }

            entityShader.Deactivate();
            return entitiesInView.Count;
        }

        /// <summary>
        /// Remove all entities.
        /// </summary>
        public void RemoveAllEntities()
        {
            foreach (var entity in this.entities)
            {
                entity.Dispose();
            }

            this.entities.Clear();
            this.playerControlledEntities.Clear();
        }

        #region IEntityContainer implementation

        void IEntityContainer.AddEntity(Entity entity)
        {
            this.entities.Add(entity);
        }

        void IEntityContainer.RemoveEntity(Entity entity)
        {
            this.entities.Remove(entity);
            this.playerControlledEntities.Remove(entity);
        }

        #endregion

        #region IAlignmentTester implementation

        void IAlignmentTester.AddEntity(Entity entity)
        {
            if (!this.alignedEntities.ContainsKey(entity.Alignment))
                this.alignedEntities[entity.Alignment] = new HashSet<Entity>();

            this.alignedEntities[entity.Alignment].Add(entity);
        }

        void IAlignmentTester.RemoveEntity(Entity entity)
        {
            this.alignedEntities[entity.Alignment].Remove(entity);
        }

        public HashSet<Entity> TestAlignedEntity(Entity entity, Func<Entity, Entity, bool> func)
        {
            HashSet<Entity> entities = new HashSet<Entity>();

            foreach (var kvp in this.alignedEntities)
            {
                if (kvp.Key == entity.Alignment)
                    continue;

                foreach (var hostileEntity in kvp.Value)
                {
                    if (func(entity, hostileEntity))
                        entities.Add(hostileEntity);
                }
            }

            return entities;
        }

        #endregion
    }
}
